/*
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 *
 * This software is open source.
 * See the bottom of this file for the licence.
 */

package org.dom4j.io;

import java.util.List;

import org.dom4j.Attribute;
import org.dom4j.CDATA;
import org.dom4j.Comment;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Entity;
import org.dom4j.Namespace;
import org.dom4j.ProcessingInstruction;
import org.dom4j.Text;
import org.dom4j.tree.NamespaceStack;
import org.w3c.dom.DOMImplementation;

/**
 * <p> <code>DOMWriter</code> takes a DOM4J tree and outputs it as a W3C DOM
 * object </p>
 * 
 * @author <a href="mailto:james.strachan@metastuff.com">James Strachan </a>
 * @version $Revision: 1.8 $
 */
public abstract class AbstractDOMWriter
{
   protected static boolean loggedWarning = false;

   private static final String[] DEFAULT_DOM_DOCUMENT_CLASSES =
   { "org.apache.xerces.dom.DocumentImpl", // Xerces
      "gnu.xml.dom.DomDocument", // GNU JAXP
      "org.apache.crimson.tree.XmlDocument", // Crimson
      "com.sun.xml.tree.XmlDocument", // Sun's Project X
      "oracle.xml.parser.v2.XMLDocument", // Oracle V2
      "oracle.xml.parser.XMLDocument", // Oracle V1
      "org.dom4j.dom.DOMDocument" // Internal DOM implementation
   };

   // the Class used to create new DOM Document instances
   protected Class domDocumentClass;

   /** stack of <code>Namespace</code> objects */
   private NamespaceStack namespaceStack = new NamespaceStack();

   protected abstract org.w3c.dom.Document createDomDocument(Document document)
      throws DocumentException;

   protected abstract org.w3c.dom.Document createDomDocument(Document document,
      DOMImplementation domImpl)     throws DocumentException;

   
   public AbstractDOMWriter()
   {
   }

   public AbstractDOMWriter(Class domDocumentClass)
   {
      this.domDocumentClass = domDocumentClass;
   }

   public Class getDomDocumentClass() throws DocumentException
   {
      Class result = domDocumentClass;

      if(result == null)
      {
         // lets try and find one in the classpath
         int size = DEFAULT_DOM_DOCUMENT_CLASSES.length;

         for(int i = 0; i < size; i++)
         {
            try
            {
               String name = DEFAULT_DOM_DOCUMENT_CLASSES[i];
               result =
                  Class.forName(name, true, DOMWriter.class.getClassLoader());

               if(result != null)
               {
                  break;
               }
            }
            catch(Exception e)
            {
               // could not load class correctly
               // lets carry on to the next one
            }
         }
      }

      return result;
   }

   /**
    * Sets the DOM {@link org.w3c.dom.Document}implementation class used by the
    * writer when creating DOM documents.
    * 
    * @param domDocumentClass is the Class implementing the
    *           {@linkorg.w3c.dom.Document} interface
    */
   public void setDomDocumentClass(Class domDocumentClass)
   {
      this.domDocumentClass = domDocumentClass;
   }

   /**
    * Sets the DOM {@link org.w3c.dom.Document}implementation class name used
    * by the writer when creating DOM documents.
    * 
    * @param name is the name of the Class implementing the {@link
    *           org.w3c.dom.Document} interface
    * 
    * @throws DocumentException if the class could not be loaded
    */
   public void setDomDocumentClassName(String name) throws DocumentException
   {
      try
      {
         this.domDocumentClass =
            Class.forName(name, true, DOMWriter.class.getClassLoader());
      }
      catch(Exception e)
      {
         throw new DocumentException("Could not load the DOM Document "
            + "class: " + name, e);
      }
   }
   
   protected interface Creator
   {
      org.w3c.dom.Document create() throws DocumentException;
   }

   protected org.w3c.dom.Document write(Document document, Creator c)
      throws DocumentException
   {
      if(document instanceof org.w3c.dom.Document)
      {
         return (org.w3c.dom.Document) document;
      }
   
      resetNamespaceStack();
   
      org.w3c.dom.Document domDocument = c.create();
      appendDOMTree(domDocument, domDocument, document.content());
      namespaceStack.clear();
   
      return domDocument;
   }
   
   public org.w3c.dom.Document write(final Document document)
      throws DocumentException
   {
      return write(document, new Creator()
      {
         public org.w3c.dom.Document create() throws DocumentException
         {
            return createDomDocument(document);
         }
         
      });
   }

   public org.w3c.dom.Document write(final Document document,
      final org.w3c.dom.DOMImplementation domImpl) throws DocumentException
   {
      return write(document, new Creator()
      {
         public org.w3c.dom.Document create() throws DocumentException
         {
            return createDomDocument(document, domImpl);
         }         
      });
   }

   protected void appendDOMTree(org.w3c.dom.Document domDocument,
      org.w3c.dom.Node domCurrent, List content)
   {
      for(Object object : content)
      {
         org.w3c.dom.Node n = appendDOMTreeInner(domDocument, domCurrent, object);
         if(n != null)
            domCurrent.appendChild(n);         
      }
   }
   
   protected org.w3c.dom.Node appendDOMTreeInner(org.w3c.dom.Document domDocument,
      org.w3c.dom.Node domCurrent, Object object)
   {
      if(object instanceof Element)
      {
         appendDOMTree(domDocument, domCurrent, (Element) object);
         return null;
      }
      
      if(object instanceof String)
         return domDocument.createTextNode((String) object);

      if(object instanceof Text)
         return domDocument.createTextNode(((Text) object).getText());

      if(object instanceof CDATA)
         return domDocument.createCDATASection(((CDATA) object).getText());

      if(object instanceof Comment)
         return domDocument.createComment(((Comment) object).getText());

      if(object instanceof Entity)
         return domDocument.createEntityReference(((Entity) object).getName());

      if(object instanceof ProcessingInstruction)
      {
         ProcessingInstruction pi = (ProcessingInstruction) object;
         return domDocument.createProcessingInstruction(pi.getTarget(), pi.getText());
      }      
      
      return null;
   }

   protected void writeNamespace(org.w3c.dom.Element domElement,
      Namespace namespace)
   {
      String attributeName = attributeNameForNamespace(namespace);

      // domElement.setAttributeNS("", attributeName, namespace.getURI());
      domElement.setAttribute(attributeName, namespace.getURI());
   }

   protected void appendDOMTree(org.w3c.dom.Document domDocument,
      org.w3c.dom.Node domCurrent, Element element)
   {
      String elUri = element.getNamespaceURI();
      String elName = element.getQualifiedName();
      org.w3c.dom.Element domElement =
         domDocument.createElementNS(elUri, elName);

      int stackSize = namespaceStack.size();

      // add the namespace of the element first
      Namespace elementNamespace = element.getNamespace();

      if(isNamespaceDeclaration(elementNamespace))
      {
         namespaceStack.push(elementNamespace);
         writeNamespace(domElement, elementNamespace);
      }

      // add the additional declared namespaces
      List declaredNamespaces = element.declaredNamespaces();

      for(int i = 0, size = declaredNamespaces.size(); i < size; i++)
      {
         Namespace namespace = (Namespace) declaredNamespaces.get(i);

         if(isNamespaceDeclaration(namespace))
         {
            namespaceStack.push(namespace);
            writeNamespace(domElement, namespace);
         }
      }

      // add the attributes
      for(int i = 0, size = element.attributeCount(); i < size; i++)
      {
         Attribute attribute = (Attribute) element.attribute(i);
         String attUri = attribute.getNamespaceURI();
         String attName = attribute.getQualifiedName();
         String value = attribute.getValue();
         domElement.setAttributeNS(attUri, attName, value);
      }

      // add content
      appendDOMTree(domDocument, domElement, element.content());

      domCurrent.appendChild(domElement);

      while(namespaceStack.size() > stackSize)
      {
         namespaceStack.pop();
      }
   }

   
   protected String attributeNameForNamespace(Namespace namespace)
   {
      String xmlns = "xmlns";
      String prefix = namespace.getPrefix();

      if(prefix.length() > 0)
      {
         return xmlns + ":" + prefix;
      }

      return xmlns;
   }

   protected boolean isNamespaceDeclaration(Namespace ns)
   {
      if((ns != null) && (ns != Namespace.NO_NAMESPACE)
         && (ns != Namespace.XML_NAMESPACE))
      {
         String uri = ns.getURI();

         if((uri != null) && (uri.length() > 0))
         {
            if(!namespaceStack.contains(ns))
            {
               return true;
            }
         }
      }

      return false;
   }

   protected void resetNamespaceStack()
   {
      namespaceStack.clear();
      namespaceStack.push(Namespace.XML_NAMESPACE);
   }
}
